using System;
using System.IO;
using System.Linq;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.IO;

namespace SharpCompress.Common.Zip;

internal class ZipHeaderFactory
{
    internal const uint ENTRY_HEADER_BYTES = 0x04034b50;
    internal const uint POST_DATA_DESCRIPTOR = 0x08074b50;
    internal const uint DIRECTORY_START_HEADER_BYTES = 0x02014b50;
    internal const uint DIRECTORY_END_HEADER_BYTES = 0x06054b50;
    internal const uint DIGITAL_SIGNATURE = 0x05054b50;
    internal const uint SPLIT_ARCHIVE_HEADER_BYTES = 0x30304b50;

    internal const uint ZIP64_END_OF_CENTRAL_DIRECTORY = 0x06064b50;
    internal const uint ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR = 0x07064b50;

    protected LocalEntryHeader? _lastEntryHeader;
    private readonly string? _password;
    private readonly StreamingMode _mode;
    private readonly ArchiveEncoding _archiveEncoding;

    protected ZipHeaderFactory(
        StreamingMode mode,
        string? password,
        ArchiveEncoding archiveEncoding
    )
    {
        _mode = mode;
        _password = password;
        _archiveEncoding = archiveEncoding;
    }

    protected ZipHeader? ReadHeader(uint headerBytes, BinaryReader reader, bool zip64 = false)
    {
        switch (headerBytes)
        {
            case ENTRY_HEADER_BYTES:
            {
                var entryHeader = new LocalEntryHeader(_archiveEncoding);
                entryHeader.Read(reader);
                LoadHeader(entryHeader, reader.BaseStream);

                _lastEntryHeader = entryHeader;
                return entryHeader;
            }
            case DIRECTORY_START_HEADER_BYTES:
            {
                var entry = new DirectoryEntryHeader(_archiveEncoding);
                entry.Read(reader);
                return entry;
            }
            case POST_DATA_DESCRIPTOR:
            {
                if (
                    _lastEntryHeader != null
                    && FlagUtility.HasFlag(
                        _lastEntryHeader.NotNull().Flags,
                        HeaderFlags.UsePostDataDescriptor
                    )
                )
                {
                    _lastEntryHeader.Crc = reader.ReadUInt32();
                    _lastEntryHeader.CompressedSize = zip64
                        ? (long)reader.ReadUInt64()
                        : reader.ReadUInt32();
                    _lastEntryHeader.UncompressedSize = zip64
                        ? (long)reader.ReadUInt64()
                        : reader.ReadUInt32();
                }
                else
                {
                    reader.ReadBytes(zip64 ? 20 : 12);
                }
                return null;
            }
            case DIGITAL_SIGNATURE:
                return null;
            case DIRECTORY_END_HEADER_BYTES:
            {
                var entry = new DirectoryEndHeader();
                entry.Read(reader);
                return entry;
            }
            case SPLIT_ARCHIVE_HEADER_BYTES:
            {
                return new SplitHeader();
            }
            case ZIP64_END_OF_CENTRAL_DIRECTORY:
            {
                var entry = new Zip64DirectoryEndHeader();
                entry.Read(reader);
                return entry;
            }
            case ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR:
            {
                var entry = new Zip64DirectoryEndLocatorHeader();
                entry.Read(reader);
                return entry;
            }
            default:
                return null;
        }
    }

    internal static bool IsHeader(uint headerBytes)
    {
        switch (headerBytes)
        {
            case ENTRY_HEADER_BYTES:
            case DIRECTORY_START_HEADER_BYTES:
            case POST_DATA_DESCRIPTOR:
            case DIGITAL_SIGNATURE:
            case DIRECTORY_END_HEADER_BYTES:
            case SPLIT_ARCHIVE_HEADER_BYTES:
            case ZIP64_END_OF_CENTRAL_DIRECTORY:
            case ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR:
                return true;
            default:
                return false;
        }
    }

    private void LoadHeader(ZipFileEntry entryHeader, Stream stream)
    {
        if (FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.Encrypted))
        {
            if (
                !entryHeader.IsDirectory
                && entryHeader.CompressedSize == 0
                && FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.UsePostDataDescriptor)
            )
            {
                throw new NotSupportedException(
                    "SharpCompress cannot currently read non-seekable Zip Streams with encrypted data that has been written in a non-seekable manner."
                );
            }

            if (_password is null)
            {
                throw new CryptographicException("No password supplied for encrypted zip.");
            }

            entryHeader.Password = _password;

            if (entryHeader.CompressionMethod == ZipCompressionMethod.WinzipAes)
            {
                var data = entryHeader.Extra.SingleOrDefault(x =>
                    x.Type == ExtraDataType.WinZipAes
                );
                if (data != null)
                {
                    var keySize = (WinzipAesKeySize)data.DataBytes[4];

                    var salt = new byte[WinzipAesEncryptionData.KeyLengthInBytes(keySize) / 2];
                    var passwordVerifyValue = new byte[2];
                    stream.Read(salt, 0, salt.Length);
                    stream.Read(passwordVerifyValue, 0, 2);
                    entryHeader.WinzipAesEncryptionData = new WinzipAesEncryptionData(
                        keySize,
                        salt,
                        passwordVerifyValue,
                        _password
                    );

                    entryHeader.CompressedSize -= (uint)(salt.Length + 2);
                }
            }
        }

        if (entryHeader.IsDirectory)
        {
            return;
        }

        //if (FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.UsePostDataDescriptor))
        //{
        //    entryHeader.PackedStream = new ReadOnlySubStream(stream);
        //}
        //else
        //{
        switch (_mode)
        {
            case StreamingMode.Seekable:
            {
                entryHeader.DataStartPosition = stream.Position;
                stream.Position += entryHeader.CompressedSize;
                break;
            }

            case StreamingMode.Streaming:
            {
                entryHeader.PackedStream = stream;
                break;
            }

            default:
            {
                throw new InvalidFormatException("Invalid StreamingMode");
            }
        }

        //}
    }
}
